import { INFO } from "../RTC/utils";
import { IBridging, ICallbacks, IPoint, IRect } from "./Bridging";
import { DockAlign } from "./DockAlign";
import { DockType } from "./DockType";
import { get_fill_distribution as get_fill_distributions, get_horizontal_distribution as get_horizontal_distributions, get_vertical_distribution as get_vertical_distributions, rect } from "./get_distributions";
import { BaseInterval, Interval } from "./Interval";
import { IPointerEvent } from "./IPointerEvent";
import { ViewPlace } from "./ViewPlace";

export class Solution<View = any, ViewData = any, Bridging extends IBridging<View> = IBridging<View>> {
  protected _interval: Interval = BaseInterval;
  protected _dock_cell_rect: IRect = rect(0, 0, 0, 0);
  protected _bridging: Bridging;

  protected _views_list: [View[], View[], View[], View[], View[], View[]] = [[], [], [], [], [], []]
  protected _rects_list: [IRect[], IRect[], IRect[], IRect[], IRect[], IRect[]] = [[], [], [], [], [], []]

  protected _dragging_dist_x: number = 0;
  protected _dragging_dist_y: number = 0;

  protected _dock_type: DockType = DockType.Left;
  protected _full_zone: IRect = rect(0, 0, 0, 0);
  protected _dock_zone: IRect = rect(0, 0, 0, 0);
  protected _drop_main_zone: IRect = rect(0, 0, 0, 0);
  protected _main_zone: IRect = rect(0, 0, 0, 0);
  protected _dragging: View | null = null;
  protected _begin_x: number = 0;
  protected _begin_y: number = 0;
  protected _offset_x: number = 0;
  protected _offset_y: number = 0;
  protected _z_index: number = 0
  protected _dock_zone_indicator: View | null = null;
  protected _main_zone_indicator: View | null = null;
  protected _full_zone_indicator: View | null = null;
  protected _dock_align: DockAlign = DockAlign.Start;

  readonly callbacks: Set<ICallbacks<View>> = new Set<ICallbacks<View>>()
  readonly view_data_map = new Map<View, ViewData>();
  readonly data_view_map = new Map<ViewData, View>();

  get all_views(): Readonly<View[]> { return this._views_list[ViewPlace.All]; }
  get floating_views(): Readonly<View[]> { return this._views_list[ViewPlace.Floating]; }
  get docked_views(): Readonly<View[]> { return this._views_list[ViewPlace.Docked]; }
  get minimized_views(): Readonly<View[]> { return this._views_list[ViewPlace.Minimized]; }
  get maximized_views(): Readonly<View[]> { return this._views_list[ViewPlace.Maximized]; }
  protected get dock_rects(): IRect[] { return this._rects_list[ViewPlace.Docked]; };
  protected get maximized_rects(): IRect[] { return this._rects_list[ViewPlace.Maximized]; };
  protected get minimized_rects(): IRect[] { return this._rects_list[ViewPlace.Minimized]; };
  protected set dock_rects(v: IRect[]) { this._rects_list[ViewPlace.Docked] = v; };
  protected set maximized_rects(v: IRect[]) { this._rects_list[ViewPlace.Maximized] = v; };
  protected set minimized_rects(v: IRect[]) { this._rects_list[ViewPlace.Minimized] = v; };

  get main_zone(): Readonly<IRect> { return this._main_zone; }
  get drop_main_zone(): Readonly<IRect> { return this._drop_main_zone; }
  get dock_cell_rect(): Readonly<IRect> { return this._dock_cell_rect }
  get dock_align() { return this._dock_align }
  get dragging() { return this._dragging }
  get bridging() { return this._bridging; }
  get full_zone(): Readonly<IRect> { return this._full_zone; }
  get dock_type() { return this._dock_type; }

  constructor(bridging: Bridging) {
    this._bridging = bridging;
    this.callbacks.add(bridging)
    bridging.set_solution(this);
  }

  set_dock_cell_rect(v: Partial<IRect>): this {
    Object.assign(this._dock_cell_rect, v);
    return this;
  }

  set_dock_align(v: DockAlign): this {
    if (this._dock_align === v) return this;
    const prev = this._dock_align;
    this._dock_align = v;
    for (const cb of this.callbacks)
      cb.on_dock_align_changed?.(v, prev, this);
    return this;
  }

  set_interval(v: Interval): this {
    this._interval = v;
    return this;
  }

  set_bridging(v: Bridging): this {
    if (this._bridging !== v) {
      this._bridging.set_solution(null)
      this.callbacks.delete(this._bridging);

      this._bridging = v;
      this._bridging.set_solution(this)
      this.callbacks.add(v);
    }
    return this
  }

  set_full_zone(v: IRect): this {
    Object.assign(this._full_zone, v);
    return this;
  }

  set_dock_type(v: DockType): this {
    if (this._dock_type === v) return this;
    const prev = this._dock_type;
    this._dock_type = v;
    for (const cb of this.callbacks)
      cb.on_dock_type_changed?.(v, prev, this);
    return this;
  }

  get_view_place(view: View): ViewPlace | null {
    for (let p = ViewPlace.Floating; p < this._views_list.length; ++p) {
      if (this._views_list[p].indexOf(view) >= 0)
        return p;
    }
    if (this._views_list[ViewPlace.All].indexOf(view) >= 0) {
      return ViewPlace.All
    }
    return null;
  }
  get_data_by_view(view: View) {
    return this.view_data_map.get(view);
  }
  get_view_by_data(data: ViewData) {
    return this.data_view_map.get(data);
  }
  set_view_data(view: View, data: ViewData) {
    this.del_view_data(view);
    this.view_data_map.set(view, data);
    this.data_view_map.set(data, view);
  }
  del_view_data(view: View) {
    if (this.view_data_map.has(view)) {
      const data = this.view_data_map.get(view)
      this.data_view_map.delete(data!);
    }
    this.view_data_map.delete(view);
  }
  del_view_data_by_data(data: ViewData) {
    const view = this.data_view_map.get(data)
    if (view) this.view_data_map.delete(view);
    this.data_view_map.delete(data);
  }
  set_view_place(view: View, place: ViewPlace): boolean {
    let idx: number = -1;
    idx = this._views_list[ViewPlace.All].indexOf(view);
    if (idx < 0) return false;
    idx = this._views_list[place].indexOf(view);
    if (idx >= 0) return false;
    this._views_list[place].push(view);
    for (let other_place = ViewPlace.Floating; other_place < this._views_list.length; ++other_place) {
      if (other_place === place)
        continue;
      idx = this._views_list[other_place].indexOf(view);
      if (idx < 0)
        continue;
      this._views_list[other_place].splice(idx, 1);
      break;
    }
    return true
  }

  change_view_place(view: View, next_place: ViewPlace) {
    if (!this._bridging.is_place_ok(view, next_place))
      return;
    const prev_place = this.get_view_place(view);
    if (prev_place === null || prev_place === next_place) {
      return;
    }
    this.set_view_place(view, next_place)
    for (const cb of this.callbacks)
      cb.on_place_changed?.(view, next_place, prev_place)
  }

  @INFO
  dock(...list: View[]) {
    for (const view of list)
      this.change_view_place(view, ViewPlace.Docked)
    return this;
  }

  @INFO
  maximize(...list: View[]) {
    for (const view of list)
      this.change_view_place(view, ViewPlace.Maximized)
    return this;
  }

  @INFO
  restore(...list: View[]) {
    for (const view of list)
      this.change_view_place(view, ViewPlace.Floating)
    return this;
  }

  @INFO
  minimize(...list: View[]) {
    for (const view of list)
      this.change_view_place(view, ViewPlace.Minimized)
    return this;
  }

  has_view(v: View): boolean {
    return this._views_list[0].indexOf(v) >= 0;
  }


  @INFO
  add_view(...list: View[]) {
    for (const view of list) {
      if (this.has_view(view)) {
        console.warn('view alreay added!');
        continue;
      }
      this._views_list[ViewPlace.All].push(view);
      this._views_list[ViewPlace.Floating].push(view);
      for (const cb of this.callbacks)
        cb.on_add?.(view, this);
      this._bridging.set_z(view, ++this._z_index)
    }
    return this;
  }

  @INFO
  del_view(...list: View[]) {
    let idx: number = -1;
    for (const view of list) {
      for (let view_list of this._views_list) {
        idx = view_list.indexOf(view);
        if (idx >= 0) view_list.splice(idx, 1);
      }
      for (const cb of this.callbacks)
        cb.on_del?.(view, this);
      this.del_view_data(view)
    }
    return this;
  }

  @INFO
  replace_view(a: View, b: View) {
    let idx: number = -1;
    idx = this._views_list[0].indexOf(a);
    if (idx >= 0) this._views_list[0].splice(idx, 1, b);
    idx = this._views_list[2].indexOf(a);
    if (idx >= 0) this._views_list[2].splice(idx, 1, b);
    idx = this._views_list[1].indexOf(a);
    if (idx >= 0) this._views_list[1].splice(idx, 1, b);
    idx = this._views_list[3].indexOf(a);
    if (idx >= 0) this._views_list[3].splice(idx, 1, b);
    return this;
  }

  @INFO
  update_zones(): this {
    switch (this._dock_type) {
      case DockType.Top:
        this._dock_zone.x = this._full_zone.x + 0;
        this._dock_zone.y = this._full_zone.y + 0;
        this._dock_zone.w = this._full_zone.w;
        this._dock_zone.h = this._dock_cell_rect.h;
        this._drop_main_zone.x = this._full_zone.x + 0;
        this._drop_main_zone.y = this._full_zone.y + this._dock_zone.h;
        this._drop_main_zone.w = this._full_zone.w;
        this._drop_main_zone.h = this._full_zone.h - this._dock_zone.h;
        break;
      case DockType.Bottom:
        this._dock_zone.x = this._full_zone.x + 0;
        this._dock_zone.y = this._full_zone.y + this._full_zone.h - this._dock_cell_rect.h;
        this._dock_zone.w = this._full_zone.w;
        this._dock_zone.h = this._dock_cell_rect.h;
        this._drop_main_zone.x = this._full_zone.x + 0;
        this._drop_main_zone.y = this._full_zone.y + 0;
        this._drop_main_zone.w = this._full_zone.w;
        this._drop_main_zone.h = this._full_zone.h - this._dock_zone.h;
        break;
      case DockType.Left:
        this._dock_zone.x = this._full_zone.x + 0;
        this._dock_zone.y = this._full_zone.y + 0;
        this._dock_zone.w = this._dock_cell_rect.w;
        this._dock_zone.h = this._full_zone.h;
        this._drop_main_zone.x = this._full_zone.x + this._dock_zone.w;
        this._drop_main_zone.y = this._full_zone.y + 0;
        this._drop_main_zone.w = this._full_zone.w - this._dock_zone.w;
        this._drop_main_zone.h = this._full_zone.h;
        break;
      case DockType.Right:
        this._dock_zone.x = this._full_zone.x + this._full_zone.w - this._dock_cell_rect.w;
        this._dock_zone.y = this._full_zone.y + 0;
        this._dock_zone.w = this._dock_cell_rect.w;
        this._dock_zone.h = this._full_zone.h;
        this._drop_main_zone.x = 0;
        this._drop_main_zone.y = this._full_zone.y + 0;
        this._drop_main_zone.w = this._full_zone.w - this._dock_zone.w;
        this._drop_main_zone.h = this._full_zone.h;
        break;
    }

    if (!this.docked_views.length) {
      this._main_zone.x = this._full_zone.x;
      this._main_zone.y = this._full_zone.y;
      this._main_zone.w = this._full_zone.w;
      this._main_zone.h = this._full_zone.h;
    } else {
      switch (this._dock_type) {
        case DockType.Top:
          this._main_zone.x = this._full_zone.x + 0;
          this._main_zone.y = this._full_zone.y + this._dock_cell_rect.h;
          this._main_zone.w = this._full_zone.w;
          this._main_zone.h = this._full_zone.h - this._dock_cell_rect.h;
          break;
        case DockType.Bottom:
          this._main_zone.x = this._full_zone.x + 0;
          this._main_zone.y = this._full_zone.y + 0;
          this._main_zone.w = this._full_zone.w;
          this._main_zone.h = this._full_zone.h - this._dock_cell_rect.h;
          break;
        case DockType.Left:
          this._main_zone.x = this._full_zone.x + this._dock_cell_rect.w;
          this._main_zone.y = this._full_zone.y + 0;
          this._main_zone.w = this._full_zone.w - this._dock_cell_rect.w;
          this._main_zone.h = this._full_zone.h;
          break;
        case DockType.Right:
          this._main_zone.x = this._full_zone.x + 0;
          this._main_zone.y = this._full_zone.y + 0;
          this._main_zone.w = this._full_zone.w - this._dock_cell_rect.w;
          this._main_zone.h = this._full_zone.h;
          break;
      }
    }
    return this;
  }
  set_rect(view: View, rect: IRect): this {
    this.bridging.set_rect(view, rect);
    return this;
  }
  get_view_rect(view: View): IRect {
    return this.bridging.get_rect(view);
  }
  set_pos(view: View, pos: IPoint): this {
    this._bridging.set_pos(view, pos);
    return this;
  }
  get_pos(view: View): IPoint {
    return this._bridging.get_pos(view);
  }
  protected _change_pos(view: View, pos: IPoint) {
    const prev = this.bridging.get_rect(view)
    this._bridging.set_pos(view, pos);
    const rect = this.bridging.get_rect(view)
    if (prev.x !== rect.x || prev.y !== rect.y || prev.w !== rect.w || prev.h !== rect.h)
      for (const cb of this.callbacks)
        cb.on_rect_changed?.(view, rect, prev, this)
  };

  protected _change_rect(view: View, rect: IRect) {
    const prev = this.bridging.get_rect(view)
    this.set_rect(view, rect);
    if (prev.x !== rect.x || prev.y !== rect.y || prev.w !== rect.w || prev.h !== rect.h)
      for (const cb of this.callbacks)
        cb.on_rect_changed?.(view, rect, prev, this)
  }

  @INFO
  update() {

    const bridging = this._bridging
    if (!bridging) return;

    let len: number = 0
    this.update_zones();

    const { maximized_views, minimized_views, docked_views } = this;
    if (docked_views.length) {
      const align = ({
        [DockAlign.Start]: 0,
        [DockAlign.End]: 1,
        [DockAlign.Center]: 0.5
      })[this._dock_align]

      switch (this._dock_type) {
        case DockType.Top:
        case DockType.Bottom:
          this.dock_rects = get_horizontal_distributions(
            docked_views.length,
            this._dock_zone,
            this._dock_cell_rect.w,
            align
          );
          break;
        case DockType.Left:
        case DockType.Right:
          this.dock_rects = get_vertical_distributions(
            docked_views.length,
            this._dock_zone,
            this._dock_cell_rect.h,
            align
          );
          break;
      }
    }

    const { dock_rects, minimized_rects } = this

    len = Math.min(minimized_views.length, minimized_rects.length);
    for (let i = 0; i < len; i++) {
      this._change_rect(minimized_views[i], minimized_rects[i])
    }

    len = Math.min(docked_views.length, dock_rects.length);
    for (let i = 0; i < len; i++) {
      this._change_rect(docked_views[i], dock_rects[i])
    }

    if (maximized_views.length) {
      const maximized_rects = this.maximized_rects = get_fill_distributions(maximized_views.length, this._main_zone);
      len = Math.min(maximized_views.length, maximized_rects.length);
      for (let i = 0; i < len; i++) {
        this.set_rect(maximized_views[i], maximized_rects[i]);
      }
    }
    if (this._dock_zone_indicator) {
      this.set_rect(this._dock_zone_indicator, this._dock_zone)
    }
    if (this._main_zone_indicator) {
      this.set_rect(this._main_zone_indicator, this._drop_main_zone)
    }
    if (this._full_zone_indicator) {
      this.set_rect(this._full_zone_indicator, this._full_zone)
    }
    const all_views = this._views_list[ViewPlace.All];
    if (all_views.length) {
      all_views.sort((a, b) => this._bridging.get_z(b) - this._bridging.get_z(a));
      all_views.forEach((v, i) => this._bridging.set_z(v, this._z_index = i))
    }
  }
  is_free(view: View) {
    return this._views_list[ViewPlace.Floating].indexOf(view) >= 0
  }
  is_docked(view: View) {
    return this._views_list[ViewPlace.Docked].indexOf(view) >= 0
  }
  is_maximized(view: View) {
    return this._views_list[ViewPlace.Maximized].indexOf(view) >= 0
  }
  is_minimized(view: View) {
    return this._views_list[ViewPlace.Minimized].indexOf(view) >= 0
  }
  is_all_docked() {
    return this._views_list[ViewPlace.Docked].length === this._views_list[ViewPlace.All].length
  }
  raise(...list: View[]) {
    for (const view of list) {
      this._bridging.set_z(view, ++this._z_index)
    }
  }
  _is_dragout = false;
  on_drag_begin = (e: IPointerEvent<View>) => {
    if (!this._bridging) return;
    const view = e.target;
    const is_free = this.is_free(view)
    const is_docked = this.is_docked(view)
    if (!is_free && !is_docked) return;
    const dockable = this._bridging.is_place_ok(view, ViewPlace.Docked);
    this._dragging = view;
    const { x, y } = this._bridging.get_rect(this._dragging)
    this._begin_x = x;
    this._begin_y = y;
    this._offset_x = e.clientX - x;
    this._offset_y = e.clientY - y;
    for (const cb of this.callbacks)
      cb.on_drag_begin?.(this._dragging, this)
    if (dockable && is_free && this._dock_zone_indicator) {
      this._bridging.set_opacity(this._dock_zone_indicator, 0.3)
      this.raise(this._dock_zone_indicator)
    }
    if (is_docked && this._main_zone_indicator) {
      this._bridging.set_opacity(this._main_zone_indicator, 0.3)
      this.raise(this._main_zone_indicator)
    }
    this._z_index += 1;
    this.raise(this._dragging)
    this._is_dragout = false;
  }
  on_drag_move = (event: IPointerEvent<View>) => {
    if (!this._dragging) return;
    const dockable = this._bridging.is_place_ok(this._dragging, ViewPlace.Docked);
    const is_docked = this.is_docked(this._dragging);
    const is_free = this.is_free(this._dragging);
    const should_dock = this.should_dock(this._dragging);
    if (is_docked) {
      if (!should_dock) this._is_dragout = true;
      if (this._is_dragout) {
        this._dragging_dist_x = event.clientX - this._offset_x;
        this._dragging_dist_y = event.clientY - this._offset_y;
        this._start_dragging_following();
      } else {
        this._stop_dragging_following();
        let x = 0;
        let y = 0;
        switch (this.dock_type) {
          case DockType.Left:
          case DockType.Right:
            x = Math.round(this._begin_x + (event.clientX - this._offset_x - this._begin_x) * 0.4)
            y = Math.round(this._begin_y + (event.clientY - this._offset_y - this._begin_y))
            break;
          case DockType.Top:
          case DockType.Bottom:
            x = Math.round(this._begin_x + (event.clientX - this._offset_x - this._begin_x))
            y = Math.round(this._begin_y + (event.clientY - this._offset_y - this._begin_y) * 0.4)
            break;
        }
        this._change_pos(this._dragging, { x, y });
      }
    } else {
      this._dragging_dist_x = event.clientX - this._offset_x;
      this._dragging_dist_y = event.clientY - this._offset_y;
      this._start_dragging_following();
    }
    if (dockable) {
      if (is_free && this._dock_zone_indicator)
        this._bridging.set_opacity(this._dock_zone_indicator, should_dock ? 1 : 0.3)
      if (is_docked && this._main_zone_indicator)
        this._bridging.set_opacity(this._main_zone_indicator, should_dock ? 0.3 : 1)
    }
  }

  @INFO
  on_drag_end = (event: IPointerEvent<View>) => {
    const {
      _bridging: bridging,
      _dragging: dragging
    } = this;
    if (!bridging || !dragging) return;

    if (this._dock_zone_indicator)
      bridging.set_opacity(this._dock_zone_indicator, 0)
    if (this._main_zone_indicator)
      bridging.set_opacity(this._main_zone_indicator, 0)
    for (const cb of this.callbacks)
      cb.on_drag_end?.(dragging, this)
    this._dragging = null;
    this._stop_dragging_following();
  }

  should_dock(view: View): boolean {
    if (!this._bridging.is_place_ok(view, ViewPlace.Docked))
      return false;
    const { x, y, w, h } = this._bridging.get_rect(view)
    const cx = x + w / 2;
    const cy = y + h / 2;
    switch (this._dock_type) {
      case DockType.Top: return cy < this._dock_zone.h + this._dock_zone.y
      case DockType.Left: return cx < this._dock_zone.w + this._dock_zone.x
      case DockType.Right: return cx > this._dock_zone.x
      case DockType.Bottom: return cy > this._dock_zone.y
    }
  }
  release() {
    for (let i = 0; i < this._views_list[0].length; i++) {
      const ele = this._views_list[0][i];
      for (const cb of this.callbacks)
        cb.on_del?.(ele, this);
    }
    this._views_list[0].length = 0;
    this._views_list[2].length = 0;
    this.dock_rects.length = 0;
    this.maximized_rects.length = 0;
  }
  set_dock_zone_indicator(r: View | null): void {
    this._dock_zone_indicator = r;
  }
  set_main_zone_indicator(r: View | null): void {
    this._main_zone_indicator = r;
  }
  set_full_zone_indicator(r: View | null): void {
    this._full_zone_indicator = r;
  }
  protected _dragging_following_tid?: any = void 0;
  protected _start_dragging_following() {
    if (this._dragging_following_tid !== void 0) return;
    this._dragging_following_tid = this._interval.start(() => {
      if (!this._bridging || !this._dragging) return;
      const { x, y } = this._bridging.get_rect(this._dragging)
      this._change_pos(this._dragging, {
        x: Math.round(x + (this._dragging_dist_x - x) * 0.5),
        y: Math.round(y + (this._dragging_dist_y - y) * 0.5)
      });
    }, 1000 / 60)
  }
  protected _stop_dragging_following() {
    if (this._dragging_following_tid !== void 0) {
      this._interval.stop(this._dragging_following_tid);
      delete this._dragging_following_tid;
    }

    if (!this._bridging || !this._dragging) return;
    this._change_pos(
      this._dragging, {
      x: Math.round(this._dragging_dist_x),
      y: Math.round(this._dragging_dist_y)
    });
  }
}
